package com.gogant.spider.http;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public final class HtmlBeanHelper {
    private static Map<Class<?>, HtmlBeanInfo<?>> beanCache = new HashMap<Class<?>, HtmlBeanHelper.HtmlBeanInfo<?>>();

    public static <T> List<T> getBeanList(Class<T> clazz, HttpResponse resp) throws Exception {
        if (!resp.isOK() || !HtmlHelper.isHtmlResponse(resp)) {
            return null;
        }
        HtmlBeanInfo<T> info = getBeanInfo(clazz);
        List<String> htmlList = new ArrayList<String>();
        HtmlSelector beanSelector = info.getBeanSelector();
        if (beanSelector != null) {
            if (!HtmlHelper.parseHtml(resp.getTextContent(), beanSelector)) {
                return null;
            } else {
                for (String html : beanSelector.getNodeHtmlList()) {
                    htmlList.add(html);
                }
            }
        } else {
            htmlList.add(resp.getTextContent());
        }
        HtmlSelector[] fieldSelectors = info.getFieldSelectors();
        List<T> beanList = new ArrayList<T>();
        for (String html : htmlList) {
            if (!HtmlHelper.parseHtml(html, fieldSelectors)) {
                return null;
            }
            T obj = clazz.newInstance();
            boolean isAllNotMatched = true;
            for (int i = 0; i < info.fieldSetters.length; i++) {
                HtmlSelector selector = fieldSelectors[i];
                if (!selector.hasMatchedNode()) {
                    continue;
                } else {
                    isAllNotMatched = false;
                }
                FieldSetter<T> setter = info.fieldSetters[i];
                setter.set(obj, selector);
            }
            if (!isAllNotMatched) {
                beanList.add(obj);
            }
        }
        return beanList;
    }

    public static <T> T getBean(Class<T> clazz, HttpResponse resp) throws Exception {
        List<T> beanList = getBeanList(clazz, resp);
        return beanList == null || beanList.size() == 0 ? null : beanList.get(0);
    }

    private static HtmlSelector toHtmlSelector(Selector annotation) {
        String path = annotation.path();
        if (path == null || path.trim().length() == 0) {
            throw new IllegalArgumentException("path can not be empty");
        }
        return new HtmlSelector(path);
    }

    @SuppressWarnings("unchecked")
    private static <T> HtmlBeanInfo<T> getBeanInfo(Class<T> clazz) throws Exception {
        HtmlBeanInfo<T> info = (HtmlBeanInfo<T>) beanCache.get(clazz);
        if (info != null) {
            return info;
        }
        Map<String, Method> writeMethodMap = new HashMap<String, Method>();
        BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
        PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
        for (PropertyDescriptor prop : props) {
            Method method = prop.getWriteMethod();
            if (method == null || !clazz.equals(method.getDeclaringClass())) {
                continue;
            }
            writeMethodMap.put(prop.getName().toUpperCase(), method);
        }
        HtmlSelector beanSelector = null;
        Selector annotation = clazz.getAnnotation(Selector.class);
        if (annotation != null) {
            beanSelector = toHtmlSelector(annotation);
            beanSelector.setSelectHtml(true);
        }
        List<FieldSetter<T>> setterList = new ArrayList<FieldSetter<T>>();
        List<HtmlSelector> selectorList = new ArrayList<HtmlSelector>();
        for (Field field : clazz.getDeclaredFields()) {
            annotation = field.getAnnotation(Selector.class);
            if (annotation == null) {
                continue;
            }
            HtmlSelector selector = toHtmlSelector(annotation);
            Class<?> setterClass = annotation.setter();
            if (setterClass == null || !FieldSetter.class.isAssignableFrom(setterClass)) {
                throw new IllegalArgumentException("illegal setter class: " + setterClass);
            }
            String value = annotation.value();
            FieldSetter<T> setter = null;
            if (setterClass == DefaultFieldSetter.class) {
                Method writeMethod = writeMethodMap.get(field.getName().toUpperCase());
                if (writeMethod != null) {
                    setter = (FieldSetter<T>) new DefaultFieldSetter(selector, field, writeMethod, value);
                } else if (Modifier.isPublic(field.getModifiers())) {
                    setter = (FieldSetter<T>) new DefaultFieldSetter(selector, field, value);
                } else {
                    throw new IllegalArgumentException("illegal field: " + field + ", cause: field must have get method or modifier is public");
                }
            } else {
                try {
                    setter = (FieldSetter<T>) setterClass.newInstance();
                } catch (Throwable e) {
                    throw new Exception("can't create setter by class[" + setterClass.getName() + "], cause: " + e.getMessage());
                }
            }
            setterList.add(setter);
            selectorList.add(selector);
        }
        if (setterList.size() == 0) {
            throw new Exception("class must contains [" + Selector.class.getName() + "] annotation: " + clazz.getName());
        }
        info = new HtmlBeanInfo<T>();
        info.beanSelector = beanSelector;
        info.fieldSetters = setterList.toArray(new FieldSetter[setterList.size()]);
        info.fieldSelectors = selectorList.toArray(new HtmlSelector[selectorList.size()]);
        beanCache.put(clazz, info);
        return info;
    }

    static class HtmlBeanInfo<T> {
        HtmlSelector beanSelector;
        FieldSetter<T>[] fieldSetters;
        HtmlSelector[] fieldSelectors;

        public HtmlSelector getBeanSelector() {
            return beanSelector == null ? null : HtmlSelector.clone(beanSelector);
        }

        public FieldSetter<T>[] getFieldSetters() {
            return fieldSetters;
        }

        public HtmlSelector[] getFieldSelectors() {
            return fieldSelectors == null ? null : HtmlSelector.clone(fieldSelectors);
        }
    }
}
